// This snow effect is based on Codepen:
// https://codepen.io/bsehovac/pen/GPwXxq
//
// Original author of the Codepen:
// https://github.com/bsehovac
//
// To enable it on any page insert the following snippet to your HTML
// (the configuration script is optional):
// <script type="text/javascript">
//   var snowConfig = {
//     density: 1 / 90,
//     depth: 80,
//     count: 7000,
//     gravity: 100,
//     speed: 1 / 5000,
//     color: [1, 1, 1],
//     opacity: 0.2,
//     snowflake: '',
//   };
// </script>
// <script type="text/javascript" src="https://storage.yandexcloud.net/slaylines/uploads/snowflakes.min.1.0.0.js" defer></script>

class SnowProgram {
    constructor($container, config) {
      const { density, depth, count, gravity, speed, color, opacity, snowflake } =
        config || {};
  
      this.density = density || 1 / 90;
      this.depth = depth || 80;
      this.count = count || 7000;
      this.gravity = gravity || 50;
      this.speed = speed || 1 / 10000;
      this.color = color || [1, 1, 1];
      this.opacity = opacity || 0.1;
      this.snowflake =
        snowflake ||
        '';
  
      this.VERTEX_SOURCE = `
        precision highp float;
        attribute vec4 a_position;
        attribute vec4 a_color;
        attribute vec3 a_rotation;
        attribute vec3 a_speed;
        attribute float a_size;
        uniform float u_time;
        uniform mat4 u_projection;
        uniform vec3 u_worldSize;
        uniform float u_gravity;
        uniform float u_wind;
        varying vec4 v_color;
        varying float v_rotation;
        void main() {
          v_color = a_color;
          v_rotation = a_rotation.x + u_time * a_rotation.y;
          vec3 pos = a_position.xyz;
          pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
          pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
          pos.x += sin(u_time * a_speed.z) * a_rotation.z;
          pos.z += cos(u_time * a_speed.z) * a_rotation.z;
          gl_Position = u_projection * vec4(pos.xyz, a_position.w);
          gl_PointSize = (a_size / gl_Position.w) * 100.0;
        }
      `;
  
      this.FRAGMENT_SOURCE = `
        precision highp float;
        uniform sampler2D u_texture;
        varying vec4 v_color;
        varying float v_rotation;
        void main() {
          vec2 rotated = vec2(
            cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5,
            cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5
          );
          vec4 snowflake = texture2D(u_texture, rotated);
          gl_FragColor = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a);
        }
      `;
  
      this.INITIAL_BUFFERS = () => ({
        position: { size: 3, value: [] },
        color: { size: 4, value: [] },
        size: { size: 1, value: [] },
        rotation: { size: 3, value: [] },
        speed: { size: 3, value: [] },
      });
  
      this.INITIAL_UNIFORMS = () => ({
        time: { type: 'float', value: 0 },
        worldSize: { type: 'vec3', value: [0, 0, 0] },
        gravity: { type: 'float', value: this.gravity },
        wind: { type: 'float', value: 0 },
        projection: {
          type: 'mat4',
          value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        },
      });
  
      this.UNIFORM_SETTERS = {
        int: 'uniform1i',
        float: 'uniform1f',
        vec2: 'uniform2fv',
        vec3: 'uniform3fv',
        vec4: 'uniform4fv',
        mat2: 'uniformMatrix2fv',
        mat3: 'uniformMatrix3fv',
        mat4: 'uniformMatrix4fv',
      };
  
      this.CANVAS_STYLE = {
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100vw',
        height: '100vh',
        background: 'transparent',
        'pointer-events': 'none',
      };
  
      this.CAMERA = {
        fov: 60,
        near: 1,
        far: 10000,
        aspect: 1,
        z: 100,
      };
  
      this.WIND = {
        current: 0,
        force: 0.1,
        target: 0.1,
        min: 0.1,
        max: 0.25,
        easing: 0.005,
      };
  
      this.init($container);
    }
  
    init($container) {
      const $canvas = this.initCanvas();
      const gl = $canvas.getContext('webgl', { antialias: true });
  
      $container.append($canvas);
  
      this.$canvas = $canvas;
      this.gl = gl;
      this.program = this.initProgram();
      this.buffers = this.initBuffers();
      this.uniforms = this.initUniforms();
      this.texture = this.initTexture();
      this.camera = this.initCamera();
      this.wind = this.initWind();
  
      this.resize = this.resize.bind(this);
      this.update = this.update.bind(this);
  
      window.addEventListener('resize', this.resize);
    }
  
    initCanvas() {
      const $canvas = document.createElement('canvas');
  
      Object.assign($canvas.style, this.CANVAS_STYLE);
  
      return $canvas;
    }
  
    initCamera() {
      return { ...this.CAMERA };
    }
  
    initWind() {
      return { ...this.WIND };
    }
  
    initShader(type, source) {
      const { gl } = this;
      const shader = gl.createShader(type);
  
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
  
      return shader;
    }
  
    initProgram() {
      const { gl } = this;
      const vertex = this.initShader(gl.VERTEX_SHADER, this.VERTEX_SOURCE);
      const fragment = this.initShader(gl.FRAGMENT_SHADER, this.FRAGMENT_SOURCE);
      const program = gl.createProgram();
  
      gl.attachShader(program, vertex);
      gl.attachShader(program, fragment);
      gl.linkProgram(program);
      gl.useProgram(program);
  
      return program;
    }
  
    initBuffers() {
      const { gl, program } = this;
      const buffers = this.INITIAL_BUFFERS();
  
      Object.entries(buffers).forEach(([name, buffer]) => {
        buffer.location = gl.getAttribLocation(program, `a_${name}`);
        buffer.ref = gl.createBuffer();
  
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref);
        gl.enableVertexAttribArray(buffer.location);
        gl.vertexAttribPointer(
          buffer.location,
          buffer.size,
          gl.FLOAT,
          false,
          0,
          0
        );
      });
  
      return buffers;
    }
  
    updateBuffers() {
      const { gl, buffers } = this;
  
      Object.keys(buffers).forEach((name) => {
        this.setBuffer(name);
      });
    }
  
    setBuffer(name, value) {
      const { gl, buffers } = this;
      const buffer = buffers[name];
  
      buffer.value = new Float32Array(value || buffer.value);
  
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref);
      gl.bufferData(gl.ARRAY_BUFFER, buffer.value, gl.STATIC_DRAW);
    }
  
    initUniforms() {
      const { gl, program } = this;
      const uniforms = this.INITIAL_UNIFORMS();
  
      Object.entries(uniforms).forEach(([name, uniform]) => {
        uniform.location = gl.getUniformLocation(program, `u_${name}`);
      });
  
      return uniforms;
    }
  
    updateUniforms() {
      const { gl, uniforms } = this;
  
      Object.keys(uniforms).forEach((name) => {
        this.setUniform(name);
      });
    }
  
    setUniform(name, value) {
      const { gl, uniforms } = this;
      const uniform = uniforms[name];
      const setter = this.UNIFORM_SETTERS[uniform.type];
      const isMatrix = /^mat[2-4]$/i.test(uniform.type);
  
      uniform.value = value || uniform.value;
  
      if (isMatrix) {
        gl[setter](uniform.location, false, uniform.value);
      } else {
        gl[setter](uniform.location, uniform.value);
      }
    }
  
    initTexture() {
      const { gl } = this;
      const texture = gl.createTexture();
      const image = new Image();
  
      gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        gl.RGBA,
        1,
        1,
        0,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        new Uint8Array([0, 0, 0, 0])
      );
  
      image.onload = () => {
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(
          gl.TEXTURE_2D,
          0,
          gl.RGBA,
          gl.RGBA,
          gl.UNSIGNED_BYTE,
          image
        );
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      };
  
      image.src = this.snowflake;
  
      return texture;
    }
  
    initSnowflakes(vw, vh, dpi) {
      const position = [];
      const color = [];
      const size = [];
      const rotation = [];
      const speed = [];
  
      const height = 1 / this.density;
      const width = (vw / vh) * height;
      const depth = this.depth;
      const count = this.count;
      const length = (vw / vh) * count;
  
      for (let i = 0; i < length; ++i) {
        position.push(
          -width + Math.random() * width * 2,
          -height + Math.random() * height * 2,
          Math.random() * depth * 2
        );
  
        speed.push(1 + Math.random(), 1 + Math.random(), Math.random() * 10);
  
        rotation.push(
          Math.random() * 2 * Math.PI,
          Math.random() * 20,
          Math.random() * 10
        );
  
        color.push(...this.color, 0.1 + Math.random() * this.opacity);
        size.push((5 * Math.random() * 5 * vh * dpi) / 1000);
      }
  
      this.setUniform('worldSize', [width, height, depth]);
  
      this.setBuffer('position', position);
      this.setBuffer('color', color);
      this.setBuffer('rotation', rotation);
      this.setBuffer('size', size);
      this.setBuffer('speed', speed);
    }
  
    setProjection(aspect) {
      const { camera } = this;
  
      camera.aspect = aspect;
  
      const fovRad = (camera.fov * Math.PI) / 180;
      const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad);
      const rangeInv = 1.0 / (camera.near - camera.far);
  
      const m0 = f / camera.aspect;
      const m5 = f;
      const m10 = (camera.near + camera.far) * rangeInv;
      const m11 = -1;
      const m14 = camera.near * camera.far * rangeInv * 2 + camera.z;
      const m15 = camera.z;
  
      return [m0, 0, 0, 0, 0, m5, 0, 0, 0, 0, m10, m11, 0, 0, m14, m15];
    }
  
    render() {
      const { gl } = this;
  
      gl.enable(gl.BLEND);
      gl.enable(gl.CULL_FACE);
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
      gl.disable(gl.DEPTH_TEST);
  
      this.updateBuffers();
      this.updateUniforms();
      this.resize(true);
  
      this.time = {
        start: window.performance.now(),
        previous: window.performance.now(),
      };
  
      if (this.raf) window.cancelAnimationFrame(this.raf);
      this.raf = window.requestAnimationFrame(this.update);
  
      return this;
    }
  
    resize(updateSnowflakes = false) {
      const { $canvas, gl } = this;
      const vw = $canvas.offsetWidth;
      const vh = $canvas.offsetHeight;
      const aspect = vw / vh;
      const dpi = window.devicePixelRatio;
  
      $canvas.width = vw * dpi;
      $canvas.height = vh * dpi;
  
      gl.viewport(0, 0, vw * dpi, vh * dpi);
      gl.clearColor(0, 0, 0, 0);
  
      if (updateSnowflakes === true) {
        this.initSnowflakes(vw, vh, dpi);
      }
  
      this.setUniform('projection', this.setProjection(aspect));
    }
  
    update(timestamp) {
      const { gl, buffers, wind } = this;
      const elapsed = (timestamp - this.time.start) * this.speed;
      const delta = timestamp - this.time.previous;
  
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(
        gl.POINTS,
        0,
        buffers.position.value.length / buffers.position.size
      );
  
      if (Math.random() > 0.995) {
        wind.target =
          (wind.min + Math.random() * (wind.max - wind.min)) *
          (Math.random() > 0.5 ? -1 : 1);
      }
  
      wind.force += (wind.target - wind.force) * wind.easing;
      wind.current += wind.force * (delta * 0.2);
  
      this.setUniform('wind', wind.current);
      this.setUniform('time', elapsed);
  
      this.time.previous = timestamp;
  
      this.raf = window.requestAnimationFrame(this.update);
    }
  }
  
  document.addEventListener('DOMContentLoaded', () => {
    window.snow = new SnowProgram(document.body, window.snowConfig).render();
  });